Spring Security 프로젝트 설정 7 - JWT Refresh Token 재발급

✒️ 2025-05-28 14:24 내용 수정



재발급을 어디서 진행할까

참고 자료 : Learn With Ifte's Implementing Secure Refresh Tokens in Spring Boot part 2, Access Token의 한계와 Refresh Token의 필요성, Inflearn refresh token filter, Spring Security를 활용한 JWT 로그인 구현(Access Token, Refresh Token)

Access Token 재발급 과정

jwt_refresh_flow_chart.png

  1. 클라이언트에서 로그인을 진행하며 서버에 email과 password를 전송한다.
  2. 서버에선 이 정보를 DB의 사용자 정보와 비교한다.
  3. 로그인이 완료되면 서버는 Refresh Token을 DB에 저장하고, Access Token과 Refresh Token을 클라이언트에 전달한다.
    • Token 저장은 어느 DB에 할지 고민했는데, 보통은 Redis(Remote Dictionary Server)에 저장한다고 한다. 다만 현재 프로젝트는 Redis로 Token을 관리하는 기능보단 프로젝트에서 제공하는 서비스들을 구현하는데 더 시간을 쓰기 위해서 Token 저장은 데이터를 저장하고 있는 DB에 같이 저장하기로 했다.
  4. 클라이언트는 응답으로 받은 Access Token은 로컬 변수에, Refresh Token은 보안 설정을 적용한 cookie에 저장한다.
  5. 클라이언트는 이후 새 요청을 보낼 때 Access Token을 Authorization Header에 담아 서버로 API 요청을 보낸다.
  6. 만약 Access Token이 만료되었다면 서버에선 Access Token이 만료되었다는 응답을 전송한다.
  7. 클라이언트에서 응답을 받은 후 Access Token 재발급을 위해 Refresh Token을 서버에 전달한다.
  8. 서버에서 Refresh Token을 검증하고, Refresh Token이 유효하다면 Access Token 재발급을 진행하고, 만료되었다면 bad_request 응답을 전송한다.

같은 Refresh Token으로 계속해서 재발급 요청을 받지 못하게 하기


Access Token 재발급 설정

Repository 수정

package com.example.security.token;  
  
import com.example.security.user.User;  
import org.springframework.data.jpa.repository.JpaRepository;  
  
import java.util.List;  
import java.util.Optional;  
  
public interface TokenRepository extends JpaRepository<Token, String> {  
  
    // Refresh Token으로 검색  
    Optional<Token> findByRefreshToken(String token);  
  
    // 사용자로 Refresh Token 검색  
    List<Token> findAllByEmail(String email);  
}

Controller 수정

package com.example.security.auth;  
  
import jakarta.servlet.http.HttpServletRequest;  
import jakarta.servlet.http.HttpServletResponse;  
import lombok.RequiredArgsConstructor;  
import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.PostMapping;  
import org.springframework.web.bind.annotation.RequestBody;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RestController;  
  
@RestController  
@RequestMapping("/api/v1/auth")  
@RequiredArgsConstructor  
public class AuthenticationController {  
  
    private final AuthenticationService service;  
  
    // 회원가입  
    @PostMapping("/register")  
    public ResponseEntity<AuthenticationResponse> register (  
      @RequestBody RegisterRequest request  
    ) {  
        return ResponseEntity.ok(service.register(request));  
    }  
  
    // 인증  
    @PostMapping("/authenticate")  
    public ResponseEntity<AuthenticationResponse> authenticate (  
            @RequestBody AuthenticationRequest request  
    ) {  
        return ResponseEntity.ok(service.authenticate(request));  
    }  
  
    // 재발급  
    @PostMapping("/refresh-token")  
    public ResponseEntity<AuthenticationResponse> authenticate (  
        HttpServletRequest request,  
        HttpServletResponse response  
    ) {  
        return service.refreshToken(request, response);  
    }  
  
}

AuthenticationService에 재발급 메소드 추가

  1. @Transactional 추가
    • 몇몇 참고 자료에서 Service 동작에 @Transactional Annotation을 추가하였는데, 사용자 인증과 Token 발급 절차가 함께 묶여 있는 경우 특정 동작을 수행하다가 에러가 발생하여 사용자는 인증이 되었는데 Token이 저장되지 못했거나 혹은 Token 처리에 문제가 생겨 DB에 이상한 데이터가 저장되는 등의 동작을 막기 위함으로 보였다.
    • 따라서 안전한 인증 절차를 위해 @Transactional을 추가하였다.
  2. 재발급 메소드 추가
    • Access Token을 재발급 하기 위해 먼저 요청에 들어온 Authorization Header에서 Refresh Token을 추출한다.
    • Token이 존재하지 않거나 Header가 일치하지 않으면 재발급을 진행하지 않는다.
    • 추출한 Token에서 사용자 이메일 정보를 가져오고, DB에서 해당 사용자를 조회하여 사용자 일치 여부를 확인한다.
    • Spring Security 프로젝트 설정 7 - JWT Refresh Token 재발급#JwtService 수정에서 추가할 isRefreshTokenValid()메소드로 요청으로 온 Refresh Token의 유효성 검사를 진행하고, 유효하다면 Access Token과 Refresh Token을 생성한다.
      • 이 때 기존 DB에 저장된 Refresh Token을 제거하고, 새로 만든 Refresh Token을 저장한다.
    • 응답용 클래스 AuthenticationResponse에 두 Token을 담아 ResponseEntity의 Body에 담고, 상태 코드를 OK로 설정하여 반환한다.
    • 위에서 isRefreshTokenValid()메소드의 유효성 검사를 통과하지 못했다면 Access Token 재발급 절차 없이 UNAUTHORIAZED 상태를 담은 ResponseEntity를 반환한다.
package com.example.security.auth;  
  
import com.example.security.config.JwtService;  
import com.example.security.user.Role;  
import com.example.security.user.User;  
import com.example.security.user.UserRepository;  
import jakarta.servlet.http.HttpServletRequest;  
import jakarta.servlet.http.HttpServletResponse;  
import jakarta.transaction.Transactional;  
import lombok.RequiredArgsConstructor;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.http.HttpHeaders;  
import org.springframework.http.HttpStatus;  
import org.springframework.http.ResponseEntity;  
import org.springframework.security.authentication.AuthenticationManager;  
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;  
import org.springframework.security.core.userdetails.UsernameNotFoundException;  
import org.springframework.security.crypto.password.PasswordEncoder;  
import org.springframework.stereotype.Service;
  
@Service  
@RequiredArgsConstructor  
@Slf4j  
public class AuthenticationService {  
  
    // DB와 상호작용하는 사용자 repo    
    private final UserRepository repository;  
    // 비밀번호 인코더  
    private final PasswordEncoder passwordEncoder;  
    // jwt 서비스  
    private final JwtService jwtService;  
    // 사용자 신원 확인  
    private final AuthenticationManager authenticationManager;  
  
	// ... 
  
    // Access Token 재발급  
    @Transactional
    public ResponseEntity<AuthenticationResponse> refreshToken(  
            HttpServletRequest request,  
            HttpServletResponse response  
            ) {  
        // request의 authorization header에서 token 추출  
        String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);  
  
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {  
            return new ResponseEntity<>(null, HttpStatus.UNAUTHORIZED);  
        }  
  
        // Refresh Token 추출  
        String token = authHeader.substring(7);  
        // jwt로부터 사용자 이메일을 추출  
        String userEmail = jwtService.extractUsername(token);  
  
        // 검증 절차  
        // 사용자 존재 여부  
        User user = repository.findByEmail(userEmail)  
                .orElseThrow(()->new UsernameNotFoundException("No user found"));  
  
        // Refresh Token 유효성 검사  
        if (jwtService.isRefreshTokenValid(token, user)) {  
            // 유효할 경우 재발급 진행  
            String accessToken = jwtService.generateAccessToken(user);  
            String refreshToken = jwtService.generateRefreshToken(user);  
  
            // 기존에 db에 저장된 Refresh Token 제거  
            jwtService.removeUserToken(token, user);  
  
            // 토큰을 db에 저장  
            jwtService.saveUserToken(refreshToken, user);  
  
            return new ResponseEntity<>(
	            new AuthenticationResponse(
		            accessToken, 
		            refreshToken
				), 
	            HttpStatus.OK
			);  
        }  
  
        return new ResponseEntity<>(null, HttpStatus.UNAUTHORIZED);  
    }  
}
  1. 로그인을 진행하는 authenticate() 메소드에서 로그인을 진행하면 DB에 저장된 해당 사용자의 모든 Refresh Token을 제거하는 removeAllUserToken()와 생성한 Refresh Token을 DB에 저장하는 saveUserToken()를 추가한다.
    • 해당 메소드는 아래 JwtService에서 구현한다.
    • 로그인을 진행했을 때는 새로운 Access Token과 Refresh Token을 발급 받기 때문에 기존에 저장된 Refresh Token을 유지할 필요가 없어 DB에서 제거하도록 구현했다.
package com.example.security.auth;  
  
@Service  
@RequiredArgsConstructor  
@Slf4j  
public class AuthenticationService {  
  
    // DB와 상호작용하는 사용자 repo    
    private final UserRepository repository;  
    // 비밀번호 인코더  
    private final PasswordEncoder passwordEncoder;  
    // jwt 서비스  
    private final JwtService jwtService;  
    // 사용자 신원 확인  
    private final AuthenticationManager authenticationManager;  
  
	// ...
  
    // 인증 확인 - 로그인
    @Transactional  
    public AuthenticationResponse authenticate(AuthenticationRequest request) {  
        // 요청으로 들어온 사용자의 신원 확인  
        authenticationManager.authenticate(  
                new UsernamePasswordAuthenticationToken(  
                        request.getEmail(),  
                        request.getPassword()  
                )  
        );  
        // 위의 인증을 거친 사용자를 DB에 검색  
        var user = repository.findByEmail(request.getEmail())  
                .orElseThrow();  
  
        // 토큰 생성 - 사용자 정보로 생성  
        var accessToken = jwtService.generateAccessToken(user);  
        var refreshToken = jwtService.generateRefreshToken(user);  
  
        // 기존에 db에 저장된 사용자의 모든 Refresh Token 제거  
        jwtService.removeAllUserToken(user);  
  
        // 토큰을 db에 저장  
        jwtService.saveUserToken(refreshToken, user);  
  
        // 인증 응답 객체 생성  
        return AuthenticationResponse.builder()  
                .accessToken(accessToken)  
                .refreshToken(refreshToken)  
                .build();  
    }  
  
	// ...  
}

JwtService 수정

  1. Access Token과 Refresh Token의 유효성 검사를 분리
    • 현재 거의 사용자 여부와 만료 기한 비교를 위주로 검사를 진행했는데, 사용자 비교 부분에서 이후 중복된다고 판단되면 과정을 줄일 것 같다.
Token 검사항목
Access Token Token 내의 사용자 정보와 DB에 있는 사용자 정보 비교
Token의 만료 여부
Refresh Token Token 내의 사용자 정보와 DB에 있는 사용자 정보 비교
Token의 만료 여부
DB에 저장된 해당 Refresh Token 존재 여부
Token의 사용자 정보와 DB Token의 사용자 정보 비교
DB의 사용자 정보와 DB Token의 사용자 정보 비교
// Access Token 유효성 검사  
public boolean isAccessTokenValid(String token, UserDetails userDetails) {  
	final String username = extractUsername(token);  
	// 토큰의 사용자 정보가 DB의 정보와 일치 여부 + 만료 기한 확인  
	// DB에 사용자 정보가 없다면 여기서 false를 반환하여 유효하지 않음을 확인  
	return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);  
}  

// Refresh Token 유효성 검사  
public boolean isRefreshTokenValid(String token, UserDetails userDetails) {  
	final String username = extractUsername(token);  
	// DB에 저장된 토큰 정보 가져오기  
	Token dbToken = tokenRepository.findByRefreshToken(token).orElse(null);  

	// 요청에 들어온 토큰 정보 유효성  
	// DB 사용자와 토큰의 사용자 정보 일치 여부, 토큰 만료 여부  
	boolean isValidRequestToken = 
	(username.equals(userDetails.getUsername())) && !isTokenExpired(token);  

	// DB에 저장된 토큰 정보 유효성  
	// DB에 토큰 존재 여부  
	// 요청 사용자와 DB에 저장된 토큰의 사용자 정보 일치 여부  
	boolean isValidDbToken = dbToken != null  
			&& username.equals(dbToken.getEmail())  
			&& userDetails.getUsername().equals(dbToken.getEmail());  

	return isValidRequestToken && isValidDbToken;  
} 
  1. DB에 저장된 Refresh Token 제거
    • RTR 방법을 적용하기 위해 Refresh Token으로 Access Token을 재발급 받거나 로그인 및 로그아웃을 수행했을 때 DB에 저장된 Token을 제거한다.
    • tokenRepositorydelete() 메소드를 사용하여 요청으로부터 온 Token 정보로 Token Entity 인스턴스를 생성하여 메소드 매개변수로 넘겨주면 해당 인스턴스와 일치하는 데이터를 DB에서 제거한다.
    • 특정 사용자의 모든 Refresh Token을 제거하기 위해 Spring Security 프로젝트 설정 7 - JWT Refresh Token 재발급#Repository 수정에서 생성한 findAllByEmail() 메소드로 Token을 조회한 후, List.forEach() 메소드를 사용하여 리스트 내의 모든 요소에 대해 삭제 동작 tokenRepository::delete을 수행한다.
// DB와 상호작용하는 token repo
private final TokenRepository tokenRepository;

// DB에서 토큰 제거  
public void removeUserToken(String refreshToken, User user) {  
	Token token = new Token(refreshToken, user.getEmail());  
	tokenRepository.delete(token);  
} 
  
// DB에 저장된 사용자의 모든 토큰 제거  
public void removeAllUserToken(User user) {  
	List<Token> list = tokenRepository.findAllByEmail(user.getEmail());  
	list.forEachdelete;  
}  
package com.example.security.config;  
  
import com.example.security.token.Token;  
import com.example.security.token.TokenRepository;  
import com.example.security.user.User;  
import io.jsonwebtoken.Claims;  
import io.jsonwebtoken.Jwts;  
import io.jsonwebtoken.SignatureAlgorithm;  
import io.jsonwebtoken.io.Decoders;  
import io.jsonwebtoken.security.Keys;  
import lombok.RequiredArgsConstructor;  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.security.core.userdetails.UserDetails;  
import org.springframework.stereotype.Service;  
  
import java.security.Key;  
import java.util.Date;  
import java.util.HashMap;  
import java.util.List;  
import java.util.Map;  
import java.util.function.Function;  
  
@Service  
@RequiredArgsConstructor  
public class JwtService {  
    // DB와 상호작용하는 token repo    
    private final TokenRepository tokenRepository;  
  
    @Value("${app.security.jwt.secret-key}")  
    private String secretKey;  
  
    // Access Token 만료기한  
    @Value("${app.security.jwt.access-token-expiration}")  
    private long accessTokenExpiration;  
  
    // Refresh Token 만료기한  
    @Value("${app.security.jwt.refresh-token-expiration}")  
    private long refreshTokenExpiration;  
  
    // DB에 토큰 저장  
    public void saveUserToken(String refreshToken, User user) {  
        Token token = new Token(refreshToken, user.getEmail());  
        tokenRepository.save(token);  
    }  
  
    // DB에서 토큰 제거  
    public void removeUserToken(String refreshToken, User user) {  
        Token token = new Token(refreshToken, user.getEmail());  
        tokenRepository.delete(token);  
    }  
  
    // DB에 저장된 사용자의 모든 토큰 제거  
    public void removeAllUserToken(User user) {  
        List<Token> list = tokenRepository.findAllByEmail(user.getEmail());  
        list.forEachdelete;  
    }  
  
    // 토큰에서 사용자 이름 추출  
    public String extractUsername(String token) {  
        return extractClaim(token, Claims::getSubject);  
    }  
  
    // 클레임 추출  
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {  
        final Claims claims = extractAllClaims(token);  
        return claimsResolver.apply(claims);  
    }  
  
    // Access 토큰 생성 - UserDetail로만 생성  
    public String generateAccessToken(UserDetails userDetails) {  
        return generateAccessToken(new HashMap<>(), userDetails);  
    }  
  
    // Access 토큰 생성  
    public String generateAccessToken(  
            Map<String, Object> extraClaims, // 토큰에 보낼 정보  
            UserDetails userDetails  
    ) {  
  
        return generateToken(extraClaims, userDetails, accessTokenExpiration);  
    }  
  
    // Refresh 토큰 생성 - UserDetail로만 생성  
    public String generateRefreshToken(UserDetails userDetails) {  
        return generateRefreshToken(new HashMap<>(), userDetails);  
    }  
  
    // Refresh 토큰 생성  
    public String generateRefreshToken(  
            Map<String, Object> extraClaims, // 토큰에 보낼 정보  
            UserDetails userDetails  
    ) {  
        return generateToken(extraClaims, userDetails, refreshTokenExpiration);  
    }  
  
    // 토큰 생성  
    private String generateToken(  
            Map<String, Object> extraClaims, // 토큰에 보낼 정보  
            UserDetails userDetails,  
            long expireTime  
    ) {  
        return Jwts  
                .builder()  
                .setClaims(extraClaims) // 클레임 추가  
                .setSubject(userDetails.getUsername()) // subject 추가  
                .setIssuedAt(new Date(System.currentTimeMillis())) // 토큰 발행일  
                .setExpiration(new Date(System.currentTimeMillis() + expireTime)) // 만료기한  
                .signWith(getSignInKey(), SignatureAlgorithm.HS256)  
                .compact();  
    }  
  
    // Access Token 유효성 검사  
    public boolean isAccessTokenValid(String token, UserDetails userDetails) {  
        final String username = extractUsername(token);  
        // 토큰의 사용자 정보가 DB의 정보와 일치 여부 + 만료 기한 확인  
        // DB에 사용자 정보가 없다면 여기서 false를 반환하여 유효하지 않음을 확인  
        return (username.equals(userDetails.getUsername())) 
	        && !isTokenExpired(token);  
    }  
  
    // Refresh Token 유효성 검사  
    public boolean isRefreshTokenValid(String token, UserDetails userDetails) {  
        final String username = extractUsername(token);  
        // DB에 저장된 토큰 정보 가져오기  
        Token dbToken = tokenRepository.findByRefreshToken(token).orElse(null);  
  
        // 요청에 들어온 토큰 정보 유효성  
        // DB 사용자와 토큰의 사용자 정보 일치 여부, 토큰 만료 여부  
        boolean isValidRequestToken = 
        (username.equals(userDetails.getUsername())) 
        && !isTokenExpired(token);  
  
        // DB에 저장된 토큰 정보 유효성  
        // DB에 토큰 존재 여부  
        // 요청 사용자와 DB에 저장된 토큰의 사용자 정보 일치 여부  
        boolean isValidDbToken = dbToken != null  
                && username.equals(dbToken.getEmail())  
                && userDetails.getUsername().equals(dbToken.getEmail());  
  
        return isValidRequestToken && isValidDbToken;  
    }  
  
    // 토큰 만료 확인  
    public boolean isTokenExpired(String token) {  
        return extractExpiration(token).before(new Date());  
    }  
  
    // 토큰에서 만료 기한 가져오기  
    public Date extractExpiration(String token) {  
        return extractClaim(token, Claims::getExpiration);  
    }  
  
    // jwt에서 모든 클레임 추출  
    private Claims extractAllClaims(String token) {  
        return Jwts  
                .parserBuilder()  
                .setSigningKey(getSignInKey())  
                .build()  
                .parseClaimsJws(token)  
                .getBody();  
    }  
  
    // jwt 서명에 사용하는 비밀 키 생성  
    private Key getSignInKey() {  
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);  
        return Keys.hmacShaKeyFor(keyBytes);  
    }  
  
}

JwtAuthenticationFilter 수정

package com.example.security.config;  
  
import jakarta.servlet.FilterChain;  
import jakarta.servlet.ServletException;  
import jakarta.servlet.http.HttpServletRequest;  
import jakarta.servlet.http.HttpServletResponse;  
import lombok.RequiredArgsConstructor;  
import org.springframework.lang.NonNull;  
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;  
import org.springframework.security.core.context.SecurityContextHolder;  
import org.springframework.security.core.userdetails.UserDetails;  
import org.springframework.security.core.userdetails.UserDetailsService;  
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;  
import org.springframework.stereotype.Component;  
import org.springframework.web.filter.OncePerRequestFilter;  
  
import java.io.IOException;  
  
@Component  
@RequiredArgsConstructor  
public class JwtAuthenticationFilter extends OncePerRequestFilter {  
  
    private final JwtService jwtService;  
    private final UserDetailsService userDetailsService;  
  
    // 요청이 들어왔을 때 처리할 작업  
    @Override  
    protected void doFilterInternal(  
            @NonNull HttpServletRequest request, // 요청  
            @NonNull HttpServletResponse response, // 응답  
            @NonNull FilterChain filterChain // 필터들  
    ) throws ServletException, IOException {  
        // 요청으로부터 온 header의 내용 추출  
        // org.springframework.http.HttpHeaders의 HttpHeaders.AUTHORIZATION도 가능  
        final String authHeader = request.getHeader("Authorization");  
        // jwt  
        final String jwt;  
        // 사용자 이메일  
        final String userEmail;  
  
        // jwt가 없으면 요청을 이후 필터로 전달  
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {  
            // jwt는 Authorization header에 Bearer schema를 사용한다.  
            filterChain.doFilter(request, response);  
            return;  
        }  
  
        // token 추출  
        jwt = authHeader.substring(7); // "Bearer "는 7글자  
        // jwt로부터 사용자 이메일을 추출  
        userEmail = jwtService.extractUsername(jwt);  
  
        // 검증 절차  
        // 사용자가 존재하고, 아직 인증을 진행하지 않아 SecurityContextHolder에 저장되지 않았을 때  
        if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {  
            // DB에서 해당 사용자 검색  
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);  
            // jwt 유효성 확인  
            if (jwtService.isAccessTokenValid(jwt, userDetails)) {  
                // Spring SecurityContext에 업데이트에 필요한 객체  
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(  
                        userDetails,  
                        null,  
                        userDetails.getAuthorities()  
                );  
                authToken.setDetails(  
                        new WebAuthenticationDetailsSource().buildDetails(request)  
                );  
                SecurityContextHolder.getContext().setAuthentication(authToken);  
            }  
        }  
        // 항상 작업이 끝나면 다음 필터로 넘겨줘야 함  
        filterChain.doFilter(request, response);  
    }  
}

Test

  1. 애플리케이션을 실행한 후 POSTMAN이나 TalenAPI에서 http://localhost:9000/api/v1/auth/register에 POST 요청으로 회원가입 과정을 수행한다.
    • 이 때 Response로 Access Token과 Refresh Token을 발급 받고, 사진 오른쪽에 DB에선 Refresh Token이 저장되어 있다.

spring_security_refresh_token_rtr 1.png

  1. 이번엔 http://localhost:9000/api/v1/auth/authenticate POST 요청으로 로그인을 진행하여 새 Access Token과 Refresh Token을 발급 받았다.
    • 사진의 오른쪽에선 1번으로 발급 받은 Refresh Token으로 DB에 정보를 조회했더니 아무 데이터가 나오지 않는 것을 볼 수 있다.
    • 즉 로그인한 사용자의 이름으로 원래 DB에 저장되어 있던 모든 Refresh Token을 삭제하였다.

spring_security_refresh_token_rtr 2.png

  1. 로그인으로 새로 발급 받은 Refresh Token으로 DB에 Token을 조회하면 Token이 조회 되는 것을 확인할 수 있으며, 이를 통해 로그인을 수행하면 기존 Refresh Token을 제거하고 새로 생성한 Refresh Token을 DB에 저장하는 동작이 잘 이루어 지고 있음을 확인할 수 있다.

spring_security_refresh_token_rtr 3.png

  1. 다음은 Access Token 재발급 요청을 위해 http://localhost:9000/api/v1/auth/authenticate POST 요청을 작성하고, 2번에서 발급 받은 Refresh Token을 Authorization Bearer Token에 담아 요청을 전송한다.
  2. 요청이 잘 이루어졌다면 새 Access Token과 Refresh Token이 응답으로 온다.
    • 기존 Token(여기선 2번에서 발급 받은 Refresh Token)이 삭제되었는지 확인하기 위해 DB에 조회를 하면 Token이 조회되지 않는다.

spring_security_refresh_token_rtr 4.png

  1. 5번에서 발급 받은 Refresh Token으로 DB에 조회하면 데이터가 조회되는 것을 확인할 수 있으며, 3번의 결과와 마찬가지로 재발급 절차를 진행했을 때도 기존 Refresh Token이 제대로 삭제되었음을 알 수 있다.

spring_security_refresh_token_rtr 5.png

  1. 만약 임의로 변형된 Token으로 재발급 요청을 보냈다면 서버에서 Token이 유효하지 않다는 응답을 전송한다.
    • 재발급 절차가 실제로 진행되었다면 5번에서 발급 받은 Token이 DB에서 삭제될텐데, 실제로 DB에 조회해보면 Token이 그대로 남아있다.
    • 따라서 유효하지 않은 Token으로 재발급 요청을 보냈을 때 재발급이 진행되지 않았음을 확인할 수 있다.

spring_security_refresh_token_rtr 6.png